package edu.utexas.esel.jgraphpad.archpadplugin.model;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.GraphLayoutCache;

import edu.utexas.esel.jgraphpad.archpadplugin.Utils;
import edu.utexas.esel.jgraphpad.archpadplugin.graph.ElementGraphCell;

/**
 * This class manages regions in an ArchPad model.
 *
 * @author svetoslavganov@mail.utexas.edu
 *
 */
public class RegionManager {

	private ColorIterator mColorIterator =
		new ColorIterator(50, new int[] {200, 200, 200});

	private List<ElementGraphCell> mRegions = new ArrayList<ElementGraphCell>();

	private List<RegionListener> mListeners = new ArrayList<RegionListener>();

	private GraphLayoutCache mGraphLayoutCache;

	private static ElementGraphCell sRegionVertexPrototype;

	private static DefaultEdge sRegoinEdgePrototype;

	private Map<ElementGraphCell, List<ElementGraphCell>> mElementToRegionsMap =
		new HashMap<ElementGraphCell, List<ElementGraphCell>>();

	private Map<ElementGraphCell, List<ElementGraphCell>> mRegionToElementsMap =
		new HashMap<ElementGraphCell, List<ElementGraphCell>>();

	/**
	 * Gets the prototype for region vertices.
	 *
	 * @return The prototype.
	 */
    public static ElementGraphCell getRegionVertexPrototype() {
		return sRegionVertexPrototype;
	}

    /**
	 * Sets the prototype for region vertices.
	 *
	 * @param prototype The prototype.
	 */
	public static void setRegionVertexPrototype(ElementGraphCell prototype) {
		sRegionVertexPrototype = prototype;
	}

	/**
	 * Gets the prototype for region edges.
	 *
	 * @return prototype The prototype.
	 */
	public static DefaultEdge getRegoinEdgePrototype() {
		return sRegoinEdgePrototype;
	}

	/**
	 * Sets the prototype for region edges.
	 *
	 * @param prototype The prototype.
	 */
	public static void setRegoinEdgePrototype(DefaultEdge prototype) {
		sRegoinEdgePrototype = prototype;
	}

	/**
	 * Sets the {@link GraphLayoutCache} this region manager operates on.
	 *
	 * @param cache The cache.
	 */
	public void setGraphLayoutCache(GraphLayoutCache cache) {
		mGraphLayoutCache = cache;
	}

	/**
     * Creates a region that includes the <code>cells</code> and
     * has a given <code>name</code>. The method creates the region only
     * if the name is acceptable (not a duplicate). 
     *
     * @param cells The cells.
     * @param name The name.
     * @return True id the region was created, false otherwise.
     */
	public boolean createRegion(ElementGraphCell[] cells, String name) {
		if (isRegionNameAcceptable(name)) {
			ElementGraphCell region = createRegion(cells);
			Utils.setElementName(region, name);
			return true;
		}
		return false;
	}

	/**
     * Creates a region that includes the <code>cells</code>.
     *
     * Note: This method creates the region without checking if its
     *     name is acceptable. Clients are responsible for performing
     *     that check.
     *
     * @param cells The cells.
     * @returns The created region.
     */
	public ElementGraphCell createRegion(ElementGraphCell[] cells) {
		// create and insert the region cell
		ElementGraphCell region = createAndAddRegion();
		return createRegion(region, cells);
	}

	/**
     * Creates a region that includes the <code>cells</code>.
     *
     * Note: This method creates the region without checking if its
     *     name is acceptable. Clients are responsible for performing
     *     that check.
     *
     * @param cells The cells.
     * @returns The created region.
     */
	public ElementGraphCell createRegion(ElementGraphCell region,
			ElementGraphCell[] cells) {
		mRegions.add(region);

        // add members to the region
		for (ElementGraphCell cell : cells) {
        	if (!Utils.isRegion(cell)) {
        		addElementToRegion(region, cell);
        	}
        }

        notifyCreated(region);

        // force the graph to reflect the region
        mGraphLayoutCache.edit(cells, new HashMap<Object, Object>());

        return region;
	}

	/**
	 * Adds an {@link ElementGraphCell} representing the region.
	 */
	private ElementGraphCell createAndAddRegion() {
		ElementGraphCell region = (ElementGraphCell) DefaultGraphModel
	        .cloneCell(mGraphLayoutCache.getModel(), sRegionVertexPrototype);
	    region.setAttribute(ElementGraphCell.ATTRIBUTE_REGION_COLOR,
		    generateRegionColor());
        region.setAttribute(ElementGraphCell.ATTRIBUTE_ELEMENT_TYPE,
    	    ElementGraphCell.TYPE_REGION);
	    mGraphLayoutCache.insert(region);
        mGraphLayoutCache.setVisible(region, false);

        return region;
	}

	/**
	 * Generates color for a region.
	 *
	 * @return The {@link Color}.
	 */
	private Color generateRegionColor() {
		if (!mColorIterator.hasNext()) {
			// if no more colors change the step and start over
			mColorIterator.mStep = mColorIterator.mStep * 3;
			mColorIterator.reset();
		}
		return mColorIterator.next();
	}

	/**
	 * Adds an element to a region.
	 *
	 * @param region The region.
	 * @param cell The cell.
	 */
	public void addElementToRegion(ElementGraphCell region,
			ElementGraphCell element) {		
		addMemberToMappedList(mElementToRegionsMap, element, region);
		addMemberToMappedList(mRegionToElementsMap, region, element);
        addEdge(region, element);
	    notifyModified(region);
	}

	/**
	 * Adds a member to a mapped list.
	 *
	 * @param key The key to which the list is mapped.
	 * @param member The member.
	 */
	private void addMemberToMappedList(
			Map<ElementGraphCell, List<ElementGraphCell>> map,
			ElementGraphCell key, ElementGraphCell member) {
	    List<ElementGraphCell> members = map.get(key);
	    if (members == null) {
		    members = new ArrayList<ElementGraphCell>();
		    map.put(key, members);
	    }
	    members.add(member);
	}

	/**
	 * Adds an edge between two {@link ElementGraphCell}s.
	 *
	 * @param from The source.
	 * @param to The target cell.
	 */
	private void addEdge(ElementGraphCell from, ElementGraphCell to) {
		DefaultPort source = getFirstDefaultPortChild(from);
        DefaultPort target = getFirstDefaultPortChild(to);
        DefaultEdge edge = (DefaultEdge) DefaultGraphModel.cloneCell(
				mGraphLayoutCache.getModel(), sRegoinEdgePrototype);
		mGraphLayoutCache.insertEdge(edge, source, target);
		mGraphLayoutCache.setVisible(edge, false);
	}

	/**
	 * Returns the first child of a {@link ElementGraphCell} which is a
	 * {@link DefaultPort}.
	 *
	 * @param cell The cell.
	 * @return The port if such exists, null otherwise.
	 */
	private DefaultPort getFirstDefaultPortChild(ElementGraphCell cell) {
		for (int i = 0, count = cell.getChildCount(); i < count; i++) {
	        Object child = cell.getChildAt(i);
			if (child instanceof DefaultPort) {
	        	return (DefaultPort) child;
	        }
		}
		return null;
	}

	/**
	 * Removes a given region.
	 *
	 * @param region The region to remove.
	 */
	public void removeRegion(ElementGraphCell region) {
     	if (mRegions.remove(region)) {

     		List<ElementGraphCell> members = mRegionToElementsMap.get(region);
     		for (ElementGraphCell member : members) {
		    	removeElementFromRegion(region, member);
     		}

		    mGraphLayoutCache.remove(new Object[] {region}, true, true);

		    notifyRemoved(region);
     	}
	}

	/**
	 * Removes an element from a region.
	 *
	 * @param region The region.
	 * @param cell The cell.
	 */
	public void removeElementFromRegion(ElementGraphCell region,
			ElementGraphCell element) {
        List<ElementGraphCell> regions = mElementToRegionsMap.get(element);
        if (regions != null && regions.remove(region)) {
            removeMemberFromMappedList(mElementToRegionsMap,element, region);
            removeMemberFromMappedList(mRegionToElementsMap, region, element);
            removeEdge(region, element);
        	notifyModified(region);
        }
	}

	/**
	 * Removes the edge between a region and an element
	 * {@link DefaultGraphCell}s.
	 *
	 * @param region The region.
	 * @param element The element.
	 */
	private void removeEdge(DefaultGraphCell region,
			DefaultGraphCell element) {
		Object[] edges = DefaultGraphModel.getEdgesBetween(
                mGraphLayoutCache.getModel(), region, element, false);
        mGraphLayoutCache.remove(edges);
	}

	/**
	 * Removes a member to a mapped list.
	 *
	 * @param key The key to which the list is mapped.
	 * @param member The member.
	 */
	private void removeMemberFromMappedList(
			Map<ElementGraphCell, List<ElementGraphCell>> map,
			ElementGraphCell key, ElementGraphCell member) {
	    List<ElementGraphCell> members = map.get(key);
	    if (members != null) {
		    members.remove(member);
	    }
	}

	/**
	 * Returns all regions.
	 *
	 * @return The regions.
	 */
	public List<ElementGraphCell> getRegions() {
		return mRegions;
	}

	/**
	 * Adds a {@link RegionListener}.
	 *
	 * @param listener The listener.
	 * @return True if added successfully, false otherwise.
	 */
	public boolean addRegionListener(RegionListener listener) {
		return mListeners.add(listener);
	}

	/**
	 * Removes a {@link RegionListener}.
	 *
	 * @param listener The listener.
	 * @return True if removed successfully, false otherwise.
	 */
	public boolean removeRegionListener(RegionListener listener) {
		return mListeners.remove(listener);
	}

	/**
	 * Returns the regions of which the <code>cell</code> is a member.
	 *
	 * @param cell The cell.
	 * @return The regions.
	 */
	public List<ElementGraphCell> getRegions(ElementGraphCell cell) {
		return mElementToRegionsMap.get(cell);
	}

	/**
	 * Returns the region members.
	 *
	 * @param cell The region cell.
	 * @return The members.
	 */
	public List<ElementGraphCell> getMembers(ElementGraphCell cell) {
		return mRegionToElementsMap.get(cell);
	}

	/**
	 * Notifies {@link RegionListener}s for creating a region.
	 *
	 * @param region The created region.
	 */
	private void notifyCreated(ElementGraphCell region) {
		for (RegionListener listener : mListeners) {
			    listener.onCreate(region);
		}
	}

	/**
	 * Notifies {@link RegionListener}s for removing a region.
	 *
	 * @param region The removed region.
	 */
	private void notifyRemoved(ElementGraphCell region) {
		for (RegionListener listener : mListeners) {
			    listener.onRemove(region);
		}
	}

	/**
	 * Notifies {@link RegionListener}s for modifying a region.
	 *
	 * @param region The modified region.
	 */
	private void notifyModified(ElementGraphCell region) {
		for (RegionListener listener : mListeners) {
			listener.onModify(region);
		}
	}

	/**
	 * Returns if the name for a region is acceptable which means
	 * not null, longer than zero, and not duplicating the name of
	 * another region.
	 *
	 * @param name The name.
	 * @return True if the name is acceptable, false otherwise.
	 */
	public boolean isRegionNameAcceptable(String name) {
		if (name == null || name.isEmpty()) {
			return false;
		}
		for (ElementGraphCell region : mRegions) {
			String elementName = Utils.getElementName(region);
			if (name.equals(elementName)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Iterator that generates colors for regions.
	 */
	private static class ColorIterator implements Iterator<Color> {
		private static final int DEFAULT_STEP = 15;

		private int[] mColors;
		private int mColorIndex;
		private int mStep;

		/**
		 * Creates a new instance with step for generating the next color
		 * equal to {@link ColorIterator#DEFAULT_STEP} and initial color
		 * values for red, green, and blue equal to 255.
		 */
		public ColorIterator() {
			this(DEFAULT_STEP);
		}

		/**
		 * Creates a new instance with a given step for generating the
		 * next color and initial color values for red, green, and blue
		 * equal to 255.
		 *
		 * @param step The step.
		 */
		public ColorIterator(int step) {
			this(step, null);
		}

		/**
		 * Creates a new instance with a given step for generating the
		 * next color and initial color values for red, green, and blue.
		 *
		 * @param step The step.
		 * @param initialColors The initial color values.
		 */
		public ColorIterator(int step, int [] initialColors) {
			mStep = step;
			if (initialColors != null) {
			    mColors = Arrays.copyOf(initialColors, initialColors.length);
			} else {
				mColors = new int[3];
				reset();
			}
		}

		/**
		 * Resets the iterator to red, green, and blue to 255.
		 */
		public void reset() {
			for (int i = 0; i < 3; i++) {
				mColors[i] = 255;
			}
		}

		public boolean hasNext() {
			return mColors[0] > 0
			    || mColors[1] > 0
			    || mColors[2] > 0;
		}

		public Color next() {
			if (!hasNext()) {
				throw new NoSuchElementException();
			}

			Color color = new Color(mColors[0], mColors[1], mColors[2]);

			int colorValue = mColors[mColorIndex] - mStep;
			if (colorValue < 0) {
				colorValue = 0;
			}
			mColors[mColorIndex] = colorValue;
			mColorIndex = (mColorIndex + 1) % 3;

			return color;
		}

		public void remove() {
			throw new UnsupportedOperationException();	
		}
	}

	/**
	 * Listener for the events of mutating architectural regions.
	 */
	public static interface RegionListener {

		/**
		 * Called when a new region is created.
		 *
		 * @param region The created region.
		 */
		public void onCreate(ElementGraphCell region);

		/**
		 * Called when a new region is removed.
		 *
		 * @param region The removed region.
		 */
		public void onRemove(ElementGraphCell region);

		/**
		 * Called when a new region is modified (i.e. members added/removed).
		 *
		 * @param region The modified region.
		 */
		public void onModify(ElementGraphCell region);
	}
}
